昨天談 Cohesion 時,我們從 change 的角度重新看 decomposition。
我們學會了如何分辨哪些東西該聚集、哪些該拆開:
隨著同個 boundary 底下的 component 越來越多,其擁有的 “功能” 也隨之增加,那我們到底該把哪些功能從 boundary expose 出去給 clinet 使用呢?
這裡可以很直觀的到,如果一個 client 直接依賴的東西之後很容易改變,那每次它改變,client 也可能被迫跟著改。
所以在決定 public interface 之前,我們得先知道一件事:到底什麼東西會變?
假設現在有一個「滿 2000 折 100」的活動:
THRESHOLD = 2000
def discount_hundred(amount: Price) -> Price:
if amount >= THRESHOLD:
return amount - 100
return amount
如果下一次只是改成「滿 3000 折 100」,只需要修改 THRESHOLD, client 也不需要做出額外的改動。
但如果下一次活動變成「全面九折」,真正改變的就不只是某個數字,而是整套折扣的做法。
所以 code 背後通常存在一個更高層次的東西:decision。
例如:
| 決策 | 當下的選擇 | 其他可能 |
|---|---|---|
| 資料存在哪裡 | SQLite | PostgreSQL、remote API |
| 使用者如何登入 | Password | LDAP、OAuth |
| 折扣怎麼計算 | 滿額折抵 | 百分比、會員價 |
| 訂單如何通知 | LINE、Push notification |
我們現在使用的 implementation,只是某個 decision 當下的答案。而不同 decision 的改動則會增加
部分 Decision 是很容易變動的,主要原因大致有三種:
這種「一個 requirement、assumption 或 design decision 有多容易因為時間或情境而改變」的性質,可以稱為 volatility。
知道哪些 decision 比較 volatile 之後,就可以回到開頭的問題。
先想一件事:一個 decision 改變的時候,哪些地方要跟著改?
答案是所有知道它目前答案的地方。 而知道的地方越多,這次 change 就越貴。
所以方向很清楚:volatile 的 decision,知道的地方越少越好,最好只有一個 boundary。
這就是 Information Hiding:
把 volatile 的 decision 留在一個 boundary 裡,不讓 client 依賴它目前的答案。
被留在裡面的 decision,稱為這個 boundary 的 secret。
回到折扣的例子。如果 pricing 提供給 client 的是:
pricing.discount_hundred(amount)
每一個呼叫它的 client,都知道「現在的活動是折 100」。活動換成九折,這些 client 全部要改。
如果提供的是:
pricing.total_of(items)
client 只知道一件比較穩定的事:這些商品最後要付多少錢。
至於裡面用的是滿額折抵、百分比還是會員價,是 pricing 的 secret。
其實在階段二提到的 Abstraction 就是一種Information Hiding的手段;我們將底層 Decision 的實作細節藏起來,只 expose 更 general 的 function 出來使用。
Information Hiding 很容易被理解成「把 variable 設成 private」,或是「不要讓 client 看到 implementation」。
但在上面兩種寫法裡,THRESHOLD 與 function 的內容都沒有 expose 出去。Code 藏得一樣好,差別只在於 client 知不知道那個 decision。
所以這裡的 information,指的不是 code,而是關於 decision 的知識。
而這份知識最常洩漏的地方,就是 public interface 本身:
要檢查一個 decision 有沒有藏好,可以問:
只看 public interface,猜得出裡面目前選了什麼嗎?
猜得出來的,就是已經洩漏的 decision。
Information Hiding 不是「能藏就藏」。它有三個限制。
一、要有證據。
理論上,任何 decision 都可能改變。
如果一句「以後可能會改」就足以把它藏起來,我們會為大量不存在的需求提前支付 complexity。
昨天的證據排序在這裡同樣適用:已經發生過的 change,強過已知的需求來源,再強過想像中的 change。Volatility 是依據前兩者做出的判斷,不是對未來的想像。
而需要多少證據,取決於要付多少成本。在既有的 boundary 上少 expose 一樣東西,幾乎不花成本;為了還沒出現的選擇先建立新的 abstraction,需要的證據就多得多。後者之後再談。
二、藏了之後,client 能做的事會變少。
Public interface 越窄,implementer 能自由改動的空間越大,client 能依賴的東西也越少。
當 client 真的需要某個被藏起來的東西,它只剩兩條路:請 boundary 多 expose 一個功能,或是繞過 boundary 自己來。
這就是 Day15 談過的拉扯:implementer 需要空間,client 需要保證。
三、Public interface 本身也是 decision。
我們可以把 decision 藏進 boundary,卻藏不了 boundary 對外的那一面。
Public interface 上的每一樣東西,都是所有 client 一起知道的 decision,也因此最難改。
所以 Information Hiding 並沒有讓 decision 消失。它做的是一次交換:
用一個比較穩定的 decision,擋在一個比較不穩定的 decision 前面。
如果擋在前面的 interface 自己也不穩定,問題只是換了位置,而且換到更貴的地方。
回到開頭的問題:
boundary 裡的東西,哪些應該 expose 給 client?哪些應該留在裡面?
昨天與今天,其實在處理同一件事的兩半:
只做前一半,change 還是會沿著 interface 傳出去。
Boundary 藏的不是 code,而是 decision。一個 decision 只有一個地方知道,它改變的時候,就只有那裡需要改。
對 coding agent 而言,decision 特別容易洩漏,因為最省事的做法,就是把手上現成的東西直接往外傳:照著目前的做法命名,底層回傳什麼就回傳什麼,底層丟出什麼錯誤,就讓它一路往上丟。這樣的 code 能通過 type check 與 test,boundary 也都還在,階段一與階段二的工具都不會提醒我們。
所以與 agent 合作時,可以多做兩件事:
notification 可以知道」。這比「請注意封裝」具體得多,agent 也才知道該守住什麼。階段二教我們把 boundary 守住。但守得再好,如果 decision 早就寫在 interface 上,change 還是會一路傳出去。